package com.yuandian.management.socket.client;


import com.yuandian.management.socket.SocketInitConfig;
import com.yuandian.management.socket.entity.*;
import com.yuandian.management.utils.JacksonUtil;
import com.yuandian.management.utils.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

/**
 * Socket客户端
 *
 * @author: tangqingchun
 * @date: 2019-10-08
 */
public class DefaultSocketClient implements SocketClient {

    private static Logger log = LoggerFactory.getLogger(DefaultSocketClient.class);

    /**
     * 系统标识，具体请参看附录：应用标识枚举
     */
    protected String appId;
    /**
     * 模块标识，具体请参看附录：模块标识枚举
     */
    protected String moduleId;
    /**
     * 操作类型，具体请参看附录：操作类型枚举
     */
    protected Integer operType;

    /**
     * 请求体-请求头
     */
    protected SocketRequestHead head;

    /**
     * 服务器端口
     */
    private static final int UD_PROBE_PORT = SocketInitConfig.getSocketPort();
    /**
     * 请求类型，固定为1
     */
    private static final int HEAD_TYPE_REQUEST = 1;
    /**
     * 响应类型，固定为2
     */
    private static final int HEAD_TYPE_RESPONSE = 2;
    private static final Base64.Encoder encoder = Base64.getEncoder();
    private static final Base64.Decoder decoder = Base64.getDecoder();
    /**
     * 控制超时时间
     */
    public static Integer proTimeOut = 60;
    /**
     * 认证信息
     */
    public static String CREDENTIALS = "public";

    /**
     * 初始化客户端执行器
     *
     * @param appId
     * @param moduleId
     * @param operType
     */
    public DefaultSocketClient(String appId, String moduleId, Integer operType) {
        super();
        this.appId = appId;
        this.moduleId = moduleId;
        this.operType = operType;
        this.head = new SocketRequestHead(appId, moduleId, operType);
    }

    /**
     * 执行单个IP请求
     */
    @Override
    public <T extends SocketResponse> T execute(SocketRequest<T> socketRequest, String ip) throws Exception {
        return _execute(socketRequest, ip);
    }

    /**
     * 执行多个IP请求
     */
    @Override
    public <T extends SocketResponse> List<T> execute(SocketRequest<T> socketRequest, List<String> ipList) throws Exception {
        List<T> list = new ArrayList<T>();
        for (String ip : ipList) {
            T response = _execute(socketRequest, ip);
            if (response != null) {
                list.add(response);
            }
        }
        return list;
    }

    /**
     * 执行socket请求
     *
     * @param socketRequest
     * @param ip
     * @return
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    private <T extends SocketResponse> T _execute(SocketRequest<T> socketRequest, String ip) throws Exception {
        if (socketRequest == null) {
            return null;
        }
        log.info("探针服务器IP:" + ip + ":" + UD_PROBE_PORT + "----开始连接服务器");
        String socketRequestIp = ip;
        InetSocketAddress inetSocketAddress = new InetSocketAddress(socketRequestIp, UD_PROBE_PORT);
        Socket socket = new Socket();
        socket.setSoTimeout(proTimeOut * 1000);
        InputStream in = null;
        OutputStream out = null;
        //消息类型	消息长度	消息体
        //消息类型：占用1字节，固定为2个值，1表示请求消息类型， 2表示响应消息类型。
        //消息长度：占用4个字节,指定消息体的长度。
        //消息体：变长内容，内容为标准的json格式，发送时消息体采用base64编码（UTF-8），接收时需要采用base64解码（UTF-8）获取实际消息体内容。
        @SuppressWarnings("rawtypes")
		SocketRequestContent socketRequestContent = new SocketRequestContent();
        socketRequestContent.setHead(head);
        socketRequestContent.setBody(socketRequest);
        try {
            //超时时间
            socket.connect(inetSocketAddress, proTimeOut * 1000);
            in = socket.getInputStream();
            out = new DataOutputStream(socket.getOutputStream());

            // 发送验证字符串
            out.write(CREDENTIALS.getBytes());
            log.info("探针服务器IP:" + ip + "-向服务器发送认证信息成功!...等待服务器返回认证码");

            //读取验证码
            byte[] responseKey = ByteBuffer.allocate(4).array();
            readByteLength(in, responseKey, 4);
            int re = Util.getInteger(responseKey, 0, 4);

            if (re != 100) {
                log.error("探针服务器IP:" + ip + "-非法返回认证码！" + re);
                return null;
            }

            //交互对象转json与byte数组
            String jsonString = JacksonUtil.objectToString(socketRequestContent);
            //log.info("编码前消息体-->" + jsonString);
            byte[] dataBytes = encoder.encode(jsonString.getBytes("UTF-8"));
            //log.info("编码后消息体-->" + new String(dataBytes, "UTF-8"));
            //消息体长度
            int dataLength = dataBytes.length;
            //log.info("消息体长度-->" + dataLength);
            //消息体长度转字节数组
            byte[] dataLengthByte = int2byte(dataLength);
            //初始化消息数组
            byte[] message = new byte[8 + dataLength];
            //设置消息类型为1：请求消息类型
            byte[] messageType = int2byte(HEAD_TYPE_REQUEST);
            //拼接消息头数组
            System.arraycopy(messageType, 0, message, 0, 4);
            System.arraycopy(dataLengthByte, 0, message, 4, 4);
            System.arraycopy(dataBytes, 0, message, 8, dataBytes.length);
            out.write(message);

            log.info("探针服务器IP:" + ip + "-认证码正确,向服务器发送消息成功...等待服务器返回消息");
            //获取服务端返回消息
            //首先读取4个字节表示的消息类型
            byte[] responseTypeBytes = ByteBuffer.allocate(4).array();
            readByteLength(in, responseTypeBytes, 4);
            int responseType = Util.getInteger(responseTypeBytes, 0, 4);
            //log.info("服务器返回消息类型为：" + responseType);
            //判断消息类型是否为2,如果读取的值不为2,则是非法返回
            if (responseType != HEAD_TYPE_RESPONSE) {
                log.error("探针服务器IP:" + ip + "-非法返回消息类型！" + responseType);
                return null;
            }
            //读取消息体长度
            byte[] headBytes = ByteBuffer.allocate(4).array();
            readByteLength(in, headBytes, 4);
            int responseDataLength = Util.getInteger(headBytes, 0, 4);
            //log.info("服务器返回消息体数据长度为-->" + responseDataLength);
            T response = null;
            if (responseDataLength > 0) {
                byte[] bytes = ByteBuffer.allocate(responseDataLength).array();
                readByteLength(in, bytes, responseDataLength);
                String returnContent = new String(bytes, "UTF-8");
                //log.info("服务器返回消息体内容为-->" + returnContent);
                byte[] returnContentBytes = decoder.decode(returnContent.getBytes("UTF-8"));
                String returnContentDecoder = new String(returnContentBytes, "UTF-8");
                //log.info("服务器返回消息体解码后-->" + returnContentDecoder);
                SocketResponseHead head = JacksonUtil.parseObject(returnContentDecoder, "head", SocketResponseHead.class);
                response = JacksonUtil.parseObject(returnContentDecoder, "body", socketRequest.responseClass());
                response.setData(returnContentDecoder);
                response.setHead(head);
            } else {
                response = socketRequest.responseClass().newInstance();
            }
            // log.info("over...");
            response.setMessageLength(responseDataLength);
            response.setMessageType(responseType);
            return response;
        } finally {
            if (out != null) {
                out.close();
            }
            if (in != null) {
                in.close();
            }
            socket.close();
        }
    }

    /**
     * int转字节数组
     *
     * @param data
     * @return
     */
    private static byte[] int2byte(int data) {
        byte[] result = new byte[4];
        result[3] = (byte) ((data >> 24) & 0xff);
        result[2] = (byte) ((data >> 16) & 0xff);
        result[1] = (byte) ((data >> 8) & 0xff);
        result[0] = (byte) (data & 0xff);
        return result;
    }

    /**
     * 字节数组转int，解决JDK大小端问题
     *
     * @param headByte
     * @return
     */
    @SuppressWarnings("unused")
    private static int byteArrayToInt(byte[] headByte) {
        return headByte[0] & 0xFF | (headByte[1] & 0xFF) << 8 | (headByte[2] & 0xFF) << 16 | (headByte[3] & 0xFF) << 24;
    }

    private byte[] readByteLength(InputStream is, byte[] data, int length) throws IOException {
        int hasRead = 0;
        int everyRead = 0;
        while (hasRead < length) {
            everyRead = is.read(data, hasRead, length - hasRead);
            if (everyRead > 0) {
                hasRead = hasRead + everyRead;
            } else if (everyRead == -1) {
                throw new IOException("is.read(data,hasRead,length-hasRead)：" + everyRead + "流已关闭");
            }

        }
        return data;
    }

}
